from machine import Pin, reset
from time import sleep
import usocket as socket
import network
import gc
import _thread
import time

# ---------------- 配置 ----------------
WIFI_SSID      = "LAPTOP-D3RPTUAA 6358"
WIFI_PASSWORD  = "19234949992.ch"
TCP_PORT       = 8080          # ESP32-CAM → ESP32-S3
HTTP_PORT      = 9000          # ESP32-S3 → 浏览器
FIXED_IP       = '192.168.137.100'
GATEWAY        = '192.168.137.1'
SUBNET         = '255.255.255.0'
DNS            = '8.8.8.8'
# -------------------------------------

client_sockets = []            # 当前所有 HTTP 长连接
frame_lock     = _thread.allocate_lock()
last_recv_tick = time.time()   # 最近一次收到数据的时间

# ---------- Wi-Fi ----------
class WiFiConnection:
    def __init__(self, ssid, pwd, ip, gw, mask, dns):
        self.ssid, self.pwd = ssid, pwd
        self.ip_cfg = (ip, mask, gw, dns)
        self.wlan = network.WLAN(network.STA_IF)

    def connect(self):
        network.WLAN(network.AP_IF).active(False)
        self.wlan.active(True)
        sleep(0.5)
        self.wlan.ifconfig(self.ip_cfg)
        if not self.wlan.isconnected():
            self.wlan.connect(self.ssid, self.pwd)
            for t in range(30, 0, -1):
                if self.wlan.isconnected():
                    break
                print(f"Wi-Fi connecting... {t}s")
                sleep(1)
        if self.wlan.isconnected():
            print("Wi-Fi OK:", self.wlan.ifconfig())
            return self.wlan.ifconfig()[0]
        else:
            print("Wi-Fi failed")
            return None

# ---------- TCP 接收线程 ----------
def tcp_rx_thread():
    global last_recv_tick
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('0.0.0.0', TCP_PORT))
    srv.listen(1)
    print(f"TCP server listening on {TCP_PORT}")

    while True:
        try:
            cam_sock, addr = srv.accept()
            cam_sock.settimeout(5.0)
            print("ESP32-CAM connected:", addr)

            # 把 MJPEG 流直接透传给所有浏览器
            while True:
                chunk = cam_sock.recv(1024)
                if not chunk:
                    break
                last_recv_tick = time.time()

                # 立即广播给所有 HTTP 客户端
                with frame_lock:
                    bads = []
                    for c in client_sockets:
                        try:
                            c.write(chunk)          # 快速 send
                        except Exception as e:
                            bads.append(c)
                    for c in bads:
                        try:
                            c.close()
                        except:
                            pass
                        client_sockets.remove(c)
            cam_sock.close()
            print("ESP32-CAM disconnected")
        except Exception as e:
            print("TCP error:", e)

# ---------- HTTP / MJPEG ----------
HTML_PAGE = b"""

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>六足机器人上位机</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <script>
        tailwind.config = {
            theme: {
                extend: {
                    colors: {
                        primary: '#165DFF',
                        secondary: '#36D399',
                        accent: '#FF6B35',
                        dark: '#1E293B',
                        light: '#F8FAFC',
                        frame: '#E2E8F0',
                        control: '#FF5722',
                        radar: '#00FF00',
                    },
                    fontFamily: {
                        inter: ['Inter', 'system-ui', 'sans-serif'],
                    },
                }
            }
        }
    </script>
    <style type="text/tailwindcss">
        @layer utilities {
            .content-auto {
                content-visibility: auto;
            }
            .grid-layout {
                grid-template-rows: 65% 35%;
                height: 100%;
            }
            .grid-layout-right {
                grid-template-rows: 50% 50%;
                height: 100%;
            }
            .frame-border {
                border: 5px solid theme('colors.frame');
            }
            .app-height {
                height: calc(100vh);
            }
            .key-button {
                @apply w-12 h-12 flex items-center justify-center rounded-lg bg-gray-100 text-gray-700 font-medium transition-all duration-200 shadow-sm;
            }
            .key-button-active {
                @apply bg-control text-white transform scale-105 shadow-md;
            }
            .robot-arena {
                @apply relative w-full h-full bg-gray-100 rounded-lg overflow-hidden;
            }
            .robot {
                @apply absolute w-12 h-12 bg-control rounded-full flex items-center justify-center text-white transition-all duration-150 shadow-md;
            }
            .keyboard-container {
                @apply grid grid-cols-3 gap-4 justify-center max-w-[12rem] mx-auto;
            }
            .video-container {
                @apply relative w-full h-full bg-gray-100 rounded-lg overflow-hidden;
            }
            .sensor-data-container {
                @apply w-full h-full bg-gray-50 rounded-lg overflow-hidden p-4;
            }
            .sensor-grid {
                @apply grid grid-cols-2 gap-2 h-full;
            }
            .sensor-item {
                @apply bg-white p-2 h-full rounded-lg shadow-sm flex flex-col justify-center;
            }
            .sensor-label {
                @apply text-base text-gray-500 mb-1;
            }
            .sensor-value {
                @apply text-base font-semibold text-dark;
            }
            .radar-container {
                @apply w-full h-full bg-dark rounded-lg overflow-hidden relative;
            }
        }
    </style>
</head>
<body class="font-inter bg-gray-50">
    <!-- 主要内容区 -->
    <div class="relative app-height frame-border overflow-hidden w-full">
        <!-- 左右分隔线 -->
        <div class="absolute left-1/2 top-0 bottom-0 w-[5px] bg-frame -ml-[2.5px] z-10"></div>

        <!-- 左侧区域 -->
        <div class="absolute left-0 top-0 bottom-0 w-1/2 grid grid-layout">
            <!-- 左侧上下分隔线 -->
            <div class="absolute left-0 right-0 top-[65%] h-[5px] bg-frame -mt-[2.5px] z-10"></div>

            <!-- 左上区域 - ESP32-CAM视频显示 -->
            <div class="bg-white p-4 overflow-hidden relative h-[500px]"> <!-- 容器固定高度（可按需调整），确保视频有明确显示范围 -->
                <div class="absolute inset-0 bg-primary/5 z-0"></div>
                
                <!-- Flex垂直布局：让标题占固定空间，视频容器填充剩余区域 -->
                <div class="relative z-10 flex flex-col h-full">
                    <h2 class="text-xl font-bold text-dark mb-3 flex items-center">
                        <i class="fa fa-area-chart text-primary mr-2"></i>
                        摄像头
                    </h2>

                    <!-- 视频容器：填充标题下方所有空间，不隐藏溢出（因object-contain已保证画面完整） -->
                    <div class="video-container flex-grow w-full h-full"> <!-- 关键：移除overflow-hidden，避免意外遮挡 -->
                        <!-- 核心修改：object-contain 替代 object-cover，实现“完整显示+单维度填满” -->
                        <img id="esp32-cam-video" 
                             src="http://192.168.137.100:9000/stream" 
                             alt="ESP32-CAM视频流" 
                             class="w-full h-full object-contain"> <!-- object-contain：完整缩放，长/宽触达容器边界 -->
                    </div>
                </div>
                
                <div class="absolute -bottom-3 -right-3 w-16 h-16 bg-primary/10 rounded-full"></div>
            </div>

            <!-- 左下区域 - 机器人控制 -->
            <div class="bg-white p-4 overflow-hidden relative">
                <div class="absolute inset-0 bg-control/5 z-0"></div>
                <div class="relative z-10">
                    <h2 class="text-lg font-bold text-dark mb-2 flex items-center">
                        <i class="fa fa-gamepad text-control mr-2"></i>
                        机器人控制
                    </h2>

                    <!-- 步态切换按钮区域 -->
                    <div class="absolute top-6 right-4 flex flex-col gap-2 z-20">
                        <div class="flex gap-2">
                            <button id="gait-wave" class="px-2 py-1 rounded-md text-base transition-all duration-200 bg-gray-200 text-gray-700 hover:bg-gray-300">
                                Wave
                            </button>
                            <button id="gait-triangle" class="px-2 py-1 rounded-md text-base transition-all duration-200 bg-control text-white">
                                Triangle
                            </button>
                        </div>
                        <div class="ml-[calc(44px+20px)]">
                            <button id="camera-light" class="px-2 py-1 rounded-md text-base transition-all duration-200 bg-gray-200 text-gray-700 hover:bg-gray-300">
                                light
                            </button>
                        </div>
                    </div>

                    <!-- 键盘布局 -->
                    <div class="keyboard-container mt-8">
                        <div class="col-span-3 flex justify-center">
                            <button id="btn-up" class="key-button text-base">W</button>
                        </div>
                        <div class="col-span-3 grid grid-cols-3 gap-2">
                            <div class="col-start-1 flex justify-end">
                                <button id="btn-left" class="key-button text-base">A</button>
                            </div>
                            <div class="flex justify-center">
                                <button id="btn-down" class="key-button text-base">S</button>
                            </div>
                            <div class="flex justify-start">
                                <button id="btn-right" class="key-button text-base">D</button>
                            </div>
                        </div>
                    </div>

                    <div class="robot-arena">
                        <div id="robot" class="robot">
                            <i class="fa fa-arrow-up"></i>
                        </div>
                    </div>

                    <div class="mt-16 text-base text-gray-500 flex flex-wrap gap-x-5">
                        <div class="w-full sm:w-auto"><span>当前指令:</span> <span id="current-command">等待输入...</span></div>
                        <div class="w-full sm:w-auto"><span>步态模式:</span> <span id="gait-mode">Triangle</span></div>
                        <div class="w-full sm:w-auto"><span>补光灯:</span> <span id="camera-light-status">关</span></div>
                    </div>
                </div>
                <div class="absolute -bottom-3 -right-3 w-12 h-12 bg-control/10 rounded-full"></div>
            </div>
        </div>

        <!-- 右侧区域 -->
        <div class="absolute right-0 top-0 bottom-0 w-1/2 grid grid-layout-right">
            <!-- 右侧上下分隔线 -->
            <div class="absolute left-0 right-0 top-1/2 h-[5px] bg-frame -mt-[2.5px] z-10"></div>

            <!-- 右上区域 - 传感器数据显示 -->
            <div class="bg-white p-4 overflow-hidden relative">
                <div class="absolute inset-0 bg-accent/5 z-0"></div>
                <div class="relative z-10">
                    <h2 class="text-lg font-bold text-dark mb-2 flex items-center">
                        <i class="fa fa-tachometer text-accent mr-2"></i>
                        传感器数据
                    </h2>

                    <div class="sensor-data-container">
                        <div class="sensor-grid">
                            <div class="sensor-item">
                                <span class="sensor-label">环境温度</span>
                                <span class="sensor-value" id="temp-value">25.6°C</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">环境湿度</span>
                                <span class="sensor-value" id="humidity-value">48.2%</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">纬度</span>
                                <span class="sensor-value" id="latitude-value">30.1234°</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">经度</span>
                                <span class="sensor-value" id="longitude-value">120.5678°</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">海拔高度</span>
                                <span class="sensor-value" id="altitude">130M</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">电池电压</span>
                                <span class="sensor-value" id="battery-voltage">7.8V</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">电池温度</span>
                                <span class="sensor-value" id="battery-temp">32.1°C</span>
                            </div>
                            <div class="sensor-item">
                                <span class="sensor-label">开机时间</span>
                                <span class="sensor-value text-green-500" id="uptime-value">02:35:42</span>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="absolute -top-3 -right-3 w-12 h-12 bg-accent/10 rounded-full"></div>
            </div>

            <!-- 右下区域 - 360雷达点云图 -->
            <div class="bg-white p-4 overflow-hidden relative">
                <div class="absolute inset-0 bg-dark/5 z-0"></div>
                <div class="relative z-10">
                    <h2 class="text-lg font-bold text-dark mb-2 flex items-center">
                        <i class="fa fa-rss text-dark mr-2"></i>
                        360°雷达点云图
                    </h2>

                    <div class="radar-container" id="radar-container">
                        <canvas id="radar-canvas" width="400" height="350"></canvas>
                        <div class="absolute bottom-2 left-2 text-xs text-white/80">
                            <span id="radar-status">状态: 扫描中...</span>
                        </div>
                    </div>
                </div>
                <div class="absolute -bottom-3 -left-3 w-12 h-12 bg-dark/10 rounded-full"></div>
            </div>
        </div>
    </div>

    <script>
        // 全局数据结构
        const esp32Data = {
            radarPoints: [],
            robotStatus: {
                position: { x: 50, y: 50 },
                direction: 0,
                gait: 'triangle',
                radarAngle: -Math.PI / 2
            }
        };

        // DOM加载完成后初始化功能
        document.addEventListener('DOMContentLoaded', () => {
            // 视频流初始化
            initVideoStream();
            // 机器人控制
            initRobotControl();
            // 雷达
            setTimeout(initRadar, 100);
            // 传感器模拟
            initSensorDataSimulation();
            // 窗口 resize 监听
            window.addEventListener('resize', () => {
                if (radarCanvas) resizeRadarCanvas();
            });
        });

        // ====================== 视频流处理 ======================
        // 初始化视频元素
        function initVideoStream() {
            const videoElement = document.getElementById('esp32-cam-video');
            // videoElement.src = "http://192.168.137.100:9000/stream";
        }

        // ====================== 机器人控制 ======================
        function initRobotControl() {
            const robot = {
                element: document.getElementById('robot'),
                commandDisplay: document.getElementById('current-command'),
                gaitDisplay: document.getElementById('gait-mode'),
                lightStatusDisplay: document.getElementById('camera-light-status'),
                updateDisplay() {
                    this.element.style.left = `${esp32Data.robotStatus.position.x}%`;
                    this.element.style.top = `${esp32Data.robotStatus.position.y}%`;
                    this.element.style.transform = `rotate(${esp32Data.robotStatus.direction * 90}deg)`;
                    this.gaitDisplay.textContent = esp32Data.robotStatus.gait.charAt(0).toUpperCase() + esp32Data.robotStatus.gait.slice(1);
                    this.lightStatusDisplay.textContent = esp32Data.robotStatus.lightOn ? '开' : '关';
                }
            };

            if (esp32Data.robotStatus.lightOn === undefined) esp32Data.robotStatus.lightOn = false;

            const keyMappings = { 'w': 'btn-up', 's': 'btn-down', 'a': 'btn-left', 'd': 'btn-right' };

            function moveRobot(direction) {
                const speed = 5;
                const arena = document.querySelector('.robot-arena');
                const arenaRect = arena.getBoundingClientRect();
                const robotSize = robot.element.offsetWidth;
                const maxX = 100 - (robotSize / arenaRect.width * 100);
                const maxY = 100 - (robotSize / arenaRect.height * 100);
                const prevX = esp32Data.robotStatus.position.x;
                const prevY = esp32Data.robotStatus.position.y;

                switch(direction) {
                    case 'up': esp32Data.robotStatus.position.y = Math.max(0, prevY - speed); esp32Data.robotStatus.direction = 0; break;
                    case 'down': esp32Data.robotStatus.position.y = Math.min(maxY, prevY + speed); esp32Data.robotStatus.direction = 2; break;
                    case 'left': esp32Data.robotStatus.position.x = Math.max(0, prevX - speed); esp32Data.robotStatus.direction = 3; break;
                    case 'right': esp32Data.robotStatus.position.x = Math.min(maxX, prevX + speed); esp32Data.robotStatus.direction = 1; break;
                }

                robot.updateDisplay();
                robot.commandDisplay.textContent = direction;
            }

            function switchGait(gaitType) {
                document.querySelectorAll('#gait-wave, #gait-triangle').forEach(btn => {
                    btn.classList.remove('bg-control', 'text-white');
                    btn.classList.add('bg-gray-200', 'text-gray-700');
                });
                document.getElementById(`gait-${gaitType}`).classList.replace('bg-gray-200', 'bg-control');
                document.getElementById(`gait-${gaitType}`).classList.replace('text-gray-700', 'text-white');
                esp32Data.robotStatus.gait = gaitType;
                robot.updateDisplay();
            }

            function toggleLight() {
                const btn = document.getElementById('camera-light');
                esp32Data.robotStatus.lightOn = !esp32Data.robotStatus.lightOn;
                if (esp32Data.robotStatus.lightOn) {
                    btn.classList.replace('bg-gray-200', 'bg-control');
                    btn.classList.replace('text-gray-700', 'text-white');
                } else {
                    btn.classList.replace('bg-control', 'bg-gray-200');
                    btn.classList.replace('text-white', 'text-gray-700');
                }
                robot.updateDisplay();
            }

            function highlightKey(key) {
                const btn = document.getElementById(keyMappings[key.toLowerCase()]);
                if (btn) btn.classList.add('key-button-active');
            }

            function unhighlightKey(key) {
                const btn = document.getElementById(keyMappings[key.toLowerCase()]);
                if (btn) btn.classList.remove('key-button-active');
            }

            // 鼠标控制绑定
            document.getElementById('btn-up').addEventListener('mousedown', () => { highlightKey('w'); moveRobot('up'); });
            document.getElementById('btn-down').addEventListener('mousedown', () => { highlightKey('s'); moveRobot('down'); });
            document.getElementById('btn-left').addEventListener('mousedown', () => { highlightKey('a'); moveRobot('left'); });
            document.getElementById('btn-right').addEventListener('mousedown', () => { highlightKey('d'); moveRobot('right'); });
            document.addEventListener('mouseup', () => Object.keys(keyMappings).forEach(key => unhighlightKey(key)));

            // 键盘控制绑定
            document.addEventListener('keydown', (e) => {
                const key = e.key.toLowerCase();
                if (keyMappings[key]) {
                    e.preventDefault();
                    highlightKey(key);
                    moveRobot(key === 'w' ? 'up' : key === 's' ? 'down' : key === 'a' ? 'left' : 'right');
                }
            });
            document.addEventListener('keyup', (e) => {
                const key = e.key.toLowerCase();
                if (keyMappings[key]) unhighlightKey(key);
            });

            // 步态/灯光绑定
            document.getElementById('gait-wave').addEventListener('click', () => switchGait('wave'));
            document.getElementById('gait-triangle').addEventListener('click', () => switchGait('triangle'));
            document.getElementById('camera-light').addEventListener('click', toggleLight);

            robot.updateDisplay();
        }

        // ====================== 传感器数据模拟 ======================
        function initSensorDataSimulation() {
            let temp = 25.6, humidity = 48.2, batteryVoltage = 7.8, batteryTemp = 32.1, uptimeSeconds = 9342, altitude = 130;

            setInterval(() => {
                // 温度波动
                temp += (Math.random() - 0.5) * 0.4;
                temp = Math.max(20, Math.min(35, temp));
                document.getElementById('temp-value').textContent = temp.toFixed(1) + '°C';

                // 湿度波动
                humidity += (Math.random() - 0.5) * 1.2;
                humidity = Math.max(30, Math.min(70, humidity));
                document.getElementById('humidity-value').textContent = humidity.toFixed(1) + '%';

                // 电池电压
                batteryVoltage -= 0.001;
                batteryVoltage = Math.max(7.0, batteryVoltage);
                document.getElementById('battery-voltage').textContent = batteryVoltage.toFixed(1) + 'V';

                // 电池温度
                batteryTemp += (Math.random() - 0.5) * 0.6;
                batteryTemp = Math.max(30, Math.min(40, batteryTemp));
                document.getElementById('battery-temp').textContent = batteryTemp.toFixed(1) + '°C';

                // 开机时间
                uptimeSeconds += 3;
                const hours = Math.floor(uptimeSeconds / 3600).toString().padStart(2, '0');
                const minutes = Math.floor((uptimeSeconds % 3600) / 60).toString().padStart(2, '0');
                const seconds = (uptimeSeconds % 60).toString().padStart(2, '0');
                document.getElementById('uptime-value').textContent = `${hours}:${minutes}:${seconds}`;

                // 海拔
                altitude += (Math.random() - 0.5) * 0.4;
                altitude = Math.max(20, Math.min(100, altitude));
                document.getElementById('altitude').textContent = altitude.toFixed(1) + 'M';

            }, 3000);
        }

        // ====================== 雷达点云图逻辑 ======================
        let radarCanvas, radarCtx;
        let radarRotation = 0;
        let radarZoom = 1;
        let radarCenterX = 200;
        let radarCenterY = 150;
        let radarRunning = true;

        function initRadar() {
            radarCanvas = document.getElementById('radar-canvas');
            radarCtx = radarCanvas.getContext('2d');
            resizeRadarCanvas();
            window.addEventListener('resize', resizeRadarCanvas);
            document.getElementById('radar-status').textContent = '状态: 扫描中...';
            animateRadar();
        }

        function resizeRadarCanvas() {
            const container = document.getElementById('radar-container');
            radarCanvas.width = container.clientWidth;
            radarCanvas.height = container.clientHeight;
            radarCenterX = radarCanvas.width / 2;
            radarCenterY = radarCanvas.height / 2;
            drawRadar();
        }

        function drawRadar() {
            radarCtx.clearRect(0, 0, radarCanvas.width, radarCanvas.height);
            drawRadarBackground();
            drawRadarPoints();
            radarRunning ? drawRadarScanLine() : drawStaticRadarScanLine();
        }

        function animateRadar() {
            if (radarRunning) {
                radarCtx.clearRect(0, 0, radarCanvas.width, radarCanvas.height);
                drawRadarBackground();
                drawRadarPoints();
                drawRadarScanLine();
                radarRotation += 0.03;
                if (radarRotation > Math.PI * 2) radarRotation -= Math.PI * 2;
                requestAnimationFrame(animateRadar);
            }
        }

        function drawRadarBackground() {
            radarCtx.fillStyle = '#000';
            radarCtx.fillRect(0, 0, radarCanvas.width, radarCanvas.height);
            radarCtx.strokeStyle = 'rgb(255,255,255)';
            radarCtx.lineWidth = 2;

            const maxRadius = Math.min(radarCanvas.width, radarCanvas.height) * 0.45;
            for (let i = 1; i <= 5; i++) {
                const radius = (maxRadius / 5) * i;
                radarCtx.beginPath();
                radarCtx.arc(radarCenterX, radarCenterY, radius / radarZoom, 0, Math.PI * 2);
                radarCtx.stroke();
            }

            radarCtx.beginPath();
            radarCtx.moveTo(radarCenterX - maxRadius / radarZoom, radarCenterY);
            radarCtx.lineTo(radarCenterX + maxRadius / radarZoom, radarCenterY);
            radarCtx.moveTo(radarCenterX, radarCenterY - maxRadius / radarZoom);
            radarCtx.lineTo(radarCenterX, radarCenterY + maxRadius / radarZoom);
            radarCtx.stroke();

            radarCtx.fillStyle = 'rgb(255,255,255)';
            radarCtx.font = '10px monospace';
            for (let i = 1; i <= 5; i++) {
                const radius = (maxRadius / 5) * i;
                const text = `${i * 50}`;
                const textWidth = radarCtx.measureText(text).width;
                radarCtx.fillText(text, radarCenterX + radius / radarZoom - textWidth - 5, radarCenterY - 5);
            }
        }

        function drawRadarPoints() {}

        function drawStaticRadarScanLine() {
            const maxRadius = Math.min(radarCanvas.width, radarCanvas.height) * 0.45;
            const staticAngle = Math.PI / 4;

            radarCtx.strokeStyle = 'rgb(255,255,255)';
            radarCtx.lineWidth = 2;
            radarCtx.beginPath();
            radarCtx.moveTo(radarCenterX, radarCenterY);
            radarCtx.lineTo(
                radarCenterX + Math.cos(staticAngle) * maxRadius / radarZoom,
                radarCenterY + Math.sin(staticAngle) * maxRadius / radarZoom
            );
            radarCtx.stroke();

            const gradient = radarCtx.createRadialGradient(
                radarCenterX, radarCenterY, 0,
                radarCenterX, radarCenterY, maxRadius / radarZoom
            );
            gradient.addColorStop(0, 'rgba(0, 255, 0, 0.05)');
            gradient.addColorStop(1, 'rgba(0, 255, 0, 0)');
            radarCtx.fillStyle = gradient;
            radarCtx.beginPath();
            radarCtx.moveTo(radarCenterX, radarCenterY);
            radarCtx.arc(radarCenterX, radarCenterY, maxRadius / radarZoom, staticAngle - 0.3, staticAngle);
            radarCtx.closePath();
            radarCtx.fill();
        }

        function drawRadarScanLine() {
            const maxRadius = Math.min(radarCanvas.width, radarCanvas.height) * 0.45;

            radarCtx.strokeStyle = 'rgb(255,255,255)';
            radarCtx.lineWidth = 2;
            radarCtx.beginPath();
            radarCtx.moveTo(radarCenterX, radarCenterY);
            radarCtx.lineTo(
                radarCenterX + Math.cos(radarRotation) * maxRadius / radarZoom,
                radarCenterY + Math.sin(radarRotation) * maxRadius / radarZoom
            );
            radarCtx.stroke();

            const gradient = radarCtx.createRadialGradient(
                radarCenterX, radarCenterY, 0,
                radarCenterX, radarCenterY, maxRadius / radarZoom
            );
            gradient.addColorStop(0, 'rgba(0, 255, 0, 0.05)');
            gradient.addColorStop(1, 'rgba(0, 255, 0, 0)');
            radarCtx.fillStyle = gradient;
            radarCtx.beginPath();
            radarCtx.moveTo(radarCenterX, radarCenterY);
            radarCtx.arc(radarCenterX, radarCenterY, maxRadius / radarZoom, radarRotation - 0.3, radarRotation);
            radarCtx.closePath();
            radarCtx.fill();
        }
    </script>
</body>
</html>

"""

def http_thread(ip):
    hdr_stream = b"""\
HTTP/1.1 200 OK\r
Content-Type: multipart/x-mixed-replace; boundary=frame\r
Cache-Control: no-cache\r
Connection: close\r
\r
"""
    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('0.0.0.0', HTTP_PORT))
    srv.listen(3)
    print(f"HTTP server: http://{ip}:{HTTP_PORT}")

    while True:
        try:
            cli, addr = srv.accept()
            req = cli.recv(1024)
            if b'GET / ' in req or b'GET /index.html' in req:
                cli.send(b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n")
                cli.send(HTML_PAGE)
                cli.close()
            elif b'GET /stream' in req:
                cli.send(hdr_stream)
                cli.setblocking(False)   # 非阻塞，方便后续 write
                with frame_lock:
                    client_sockets.append(cli)
            else:
                cli.close()
        except Exception as e:
            print("HTTP error:", e)

# ---------- 心跳/状态 ----------
def heartbeat():
    while True:
        sleep(5)
        if time.time() - last_recv_tick > 5:
            print("⚠️  No data from ESP32-CAM")
        gc.collect()

# ---------- main ----------
def main():
    gc.enable()
    wifi = WiFiConnection(WIFI_SSID, WIFI_PASSWORD,
                          FIXED_IP, GATEWAY, SUBNET, DNS)
    ip = wifi.connect()
    if not ip:
        sleep(2)
        reset()

    _thread.start_new_thread(tcp_rx_thread, ())
    _thread.start_new_thread(http_thread, (ip,))
    heartbeat()          # 主线程做心跳

if __name__ == "__main__":
    main()
    